/*******************************************************************************
 * Copyright (c) 2006,2017 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *******************************************************************************/
package org.aspectj.testing.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * @author Matthew Webster
 * @author Andy Clement
 */
public class TestServer implements Runnable {

	private static final boolean debug = Boolean.getBoolean("org.aspectj.testing.server.debug");
	// AspectJ project root folder nameing pattern, case-insensitive (i.e. org.aspectj, AspectJ)
	protected static final String REGEX_PROJECT_ROOT_FOLDER = "(?i)(org[.])?aspectj";

	private boolean exitOnError = true;
	private File workingDirectory;
	private ClassLoader rootLoader;
	private Map<String,ClassLoader> loaders = new HashMap<>();

	private String mainClass = "UnknownClass";
	private String mainLoader = "UnknownLoader";

	public void initialize () throws IOException {
		createRootLoader();
		loadConfiguration();
	}

	private void loadConfiguration () throws IOException {
		File file = new File(workingDirectory,"server.properties");
		Properties props = new Properties();
		FileInputStream in = new FileInputStream(file);
		props.load(in);
		in.close();

		Enumeration<?> enu = props.propertyNames();
		while (enu.hasMoreElements()) {
			String key = (String)enu.nextElement();
			if (key.startsWith("loader.")) {
				createLoader(props.getProperty(key));
			}
			else if (key.equals("main")) {
				StringTokenizer st = new StringTokenizer(props.getProperty(key),",");
				mainClass = st.nextToken();
				mainLoader = st.nextToken();
			}
		}
	}

	private void createLoader (String property) throws IOException {
		ClassLoader parent = rootLoader;

		StringTokenizer st = new StringTokenizer(property,",");
		String name = st.nextToken();
		String classpath = st.nextToken();
		if (debug) System.err.println("Creating loader "+name+" with classpath "+classpath);
		if (st.hasMoreTokens()) {
			String parentName = st.nextToken();
			parent = loaders.get(parentName);
			if (parent == null) error("No such loader: " + parentName);
		}

		List<URL> urlList = new ArrayList<>();
		st = new StringTokenizer(classpath,";");
		while (st.hasMoreTokens()) {
			String fileName = st.nextToken();
			File file = new File(workingDirectory,fileName).getCanonicalFile();
			if (!file.exists()) error("Missing or invalid file: " + file.getPath());
			URL url = file.toURI().toURL();
			urlList.add(url);
		}
		URL[] urls = new URL[urlList.size()];
		urlList.toArray(urls);
		ClassLoader loader = new URLClassLoader(urls, parent);
		if (debug) System.err.println("? TestServer.createLoader() loader=" + loader + ", name='" + name + "', urls=" + urlList + ", parent=" + parent);

		loaders.put(name,loader);
	}

	private void createRootLoader() throws IOException {
		List<URL> urlList = new ArrayList<>();

		// Sandbox
		urlList.add(workingDirectory.getCanonicalFile().toURI().toURL());
		File projectRootFolder = findProjectRootFolder();
		urlList.add(new File(projectRootFolder,"runtime/target/classes").toURI().toURL());
//		urlList.add(new File(projectRootFolder,"aspectjrt/target/classes").toURI().toURL());
//		urlList.add(new File(projectRootFolder,"aspectj5rt/target/classes").toURI().toURL());

		URL[] urls = new URL[urlList.size()];
		urlList.toArray(urls);
		ClassLoader parent = getClass().getClassLoader().getParent();
		rootLoader = new URLClassLoader(urls,parent);
		if (debug) System.err.println("? TestServer.createRootLoader() loader=" + rootLoader + ", urlList=" + urlList + ", parent=" + parent);
	}

	// Make protected in order to at least make this part of the code testable -> TestServerTest
	protected File findProjectRootFolder() throws IOException {
		// Find the AspectJ project root folder
		File currentFolder = new File(".").getCanonicalFile();
		while (currentFolder != null && !currentFolder.getName().matches(REGEX_PROJECT_ROOT_FOLDER)) {
			currentFolder = currentFolder.getParentFile();
		}
		if (currentFolder == null) {
			error(
				"Unable to locate project root folder matching regex '" + REGEX_PROJECT_ROOT_FOLDER +
					"' in " + new File(".").getCanonicalPath()
			);
		}
		return currentFolder;
	}

	public void setExitOntError (boolean b) {
		exitOnError = b;
	}

	public void setWorkingDirectory (String name) {
		workingDirectory = new File(name);
		if (!workingDirectory.exists()) error("Missing or invalid working directory: " + workingDirectory.getPath());
	}

	public static void main(String[] args) throws Exception {
		System.out.println("Starting ...");

		TestServer server = new TestServer();
		server.setWorkingDirectory(args[0]);
		server.initialize();

		Thread thread = new Thread(server,"application");
		thread.start();
		thread.join();

		System.out.println("Stopping ...");
	}

	public void run() {
		System.out.println("Running " + mainClass);
		runClass(mainClass,loaders.get(mainLoader));
	}

	private void runClass (String className, ClassLoader classLoader) {
		try {
			Class<?> clazz = Class.forName(className,false,classLoader);
			invokeMain(clazz,new String[] {});
		}
		catch (ClassNotFoundException ex) {
			ex.printStackTrace();
			error(ex.toString());
		}
	}

	public void invokeMain (Class<?> clazz, String[] args)
	{
		Class<?>[] paramTypes = new Class[1];
		paramTypes[0] = args.getClass();

		try {
			Method method = clazz.getDeclaredMethod("main",paramTypes);
			Object[] params = new Object[1];
			params[0] = args;
			method.invoke(null,params);
		}
		catch (InvocationTargetException ex) {
			Throwable th = ex.getTargetException();
			th.printStackTrace();
			error(th.toString());
		}
		catch (Throwable th) {
			th.printStackTrace();
			error(th.toString());
		}
	}

	private void error (String message) {
		System.out.println(message);
		// FIXME:
		//   This is horrible: Why not just throw an exception? And if we exit, why with exit code 0?
		//   It basically makes the tests useless because they might log errors without checking any conditions.
		if (exitOnError) System.exit(0);
	}
}
